/************************************************************************
* render3d.c
* voxelands - 3d voxel world sandbox game
* Copyright (C) Lisa 'darkrose' Milne 2016 <lisa@ltmnet.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
************************************************************************/

#include "common.h"
#include "graphics.h"
#include "list.h"
#include "array.h"

#include <math.h>

static struct {
	array_t *objects;
	object_t *sorted;
	shader_t *shader;
	float frustum[6][4];
	matrix_t view;
} render3d_data = {
	NULL,
	NULL,
	NULL
};

static int render3d_sort(void *e1, void *e2)
{
	camera_t *c;
	v3_t cp;
	v3_t ep;
	object_t *o1;
	object_t *o2;
	GLfloat d1;
	GLfloat d2;
	c = camera_get();
	cp.x = c->x;
	cp.y = c->y;
	cp.z = c->z;

	o1 = (object_t*)e1;
	o2 = (object_t*)e2;

	ep.x = o1->pos.x;
	ep.y = o1->pos.y;
	ep.z = o1->pos.z;
	d1 = math_distance(&cp,&ep);
	if (d1 < 0)
		d1 *= -1;

	ep.x = o2->pos.x;
	ep.y = o2->pos.y;
	ep.z = o2->pos.z;
	d2 = math_distance(&cp,&ep);
	if (d2 < 0)
		d2 *= -1;

	/* return 1 if e1 is closer to the camera than e2 */
	if (d1 < d2)
		return 1;
	return 0;
}

static void render3d_genvao(mesh_t *m)
{
	if (!m->v->length || !m->i->length)
		return;

	glGenVertexArrays(1,&m->vao.list);
	glBindVertexArray(m->vao.list);

	glGenBuffers(1, &m->vao.indices);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m->vao.indices);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, m->i->length*4, m->i->data, GL_STATIC_DRAW);

	glGenBuffers(1, &m->vao.vertices);
	glBindBuffer(GL_ARRAY_BUFFER, m->vao.vertices);
	glBufferData(GL_ARRAY_BUFFER, m->v->length*4, m->v->data, GL_STATIC_DRAW);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,0);

	if (m->n && m->n->length) {
		glGenBuffers(1, &m->vao.normals);
		glBindBuffer(GL_ARRAY_BUFFER, m->vao.normals);
		glBufferData(GL_ARRAY_BUFFER, m->n->length*4, m->n->data, GL_STATIC_DRAW);
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,0,0);
	}

	if (m->t && m->t->length) {
		glGenBuffers(1, &m->vao.texcoords);
		glBindBuffer(GL_ARRAY_BUFFER, m->vao.texcoords);
		glBufferData(GL_ARRAY_BUFFER, m->t->length*4, m->t->data, GL_STATIC_DRAW);
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2,2,GL_FLOAT,GL_FALSE,0,0);
	}

	m->vao.state = 1;
}

/* find a 3d object */
object_t *render3d_object_find(int id)
{
	if (render3d_data.objects) {
		int i;
		object_t **o = render3d_data.objects->data;
		for (i=0; i<render3d_data.objects->length; i++) {
			if (o[i] && o[i]->id == id)
				return o[i];
		}
	}

	return NULL;
}

/* destroy a 3d object */
void render3d_object_free(object_t *o)
{
	mesh_t *m;
	int i = array_find_ptr(render3d_data.objects,o);
	if (i < 0)
		return;

	o->ignore = 2;

	while (o->ignore != 3) {
		delay(1);
	}

	((unsigned char**)(render3d_data.objects->data))[i] = NULL;

	if (!o->m) {
		while ((m = array_pop_ptr(o->meshes))) {
			mesh_free(m);
		}
		array_free(o->meshes,1);
	}
	free(o);
}

/* create a new 3d object */
object_t *render3d_object_create()
{
	static int object_ids = 1;
	object_t *o = malloc(sizeof(object_t));

	o->meshes = array_create(ARRAY_TYPE_PTR);
	o->id = object_ids++;
	o->anim.skeleton = 0;
	o->anim.frame = 0;
	o->anim.value = 0.0;
	o->ignore = 1;
	o->drop = 0;
	o->m = NULL;
	o->pos.x = 0;
	o->pos.y = 0;
	o->pos.z = 0;
	o->rot.x = 0;
	o->rot.y = 0;
	o->rot.z = 0;
	o->scale.x = 1.0;
	o->scale.y = 1.0;
	o->scale.z = 1.0;

	o->bounds.max.x = 0.0;
	o->bounds.max.y = 0.0;
	o->bounds.max.z = 0.0;
	o->bounds.min.x = 0.0;
	o->bounds.min.y = 0.0;
	o->bounds.min.z = 0.0;

	if (!render3d_data.objects)
		render3d_data.objects = array_create(ARRAY_TYPE_PTR);

	array_push_ptr(render3d_data.objects,o);

	return o;
}

/* render a model */
object_t *render3d_model(model_t *mod, v3_t *pos)
{
	object_t *o = render3d_object_create();
	o->pos.x = pos->x;
	o->pos.y = pos->y;
	o->pos.z = pos->z;

	o->m = mod;

	array_free(o->meshes,1);

	o->meshes = mod->meshes;

	o->ignore = 0;

	return o;
}

object_t *render3d_cube(float scale, v3_t *pos, material_t *mat)
{
	v3_t v[24] = {
		{0.5,0.5,-0.5},
		{0.5,-0.5,-0.5},
		{-0.5,-0.5,-0.5},
		{-0.5,0.5,-0.5},
		{-0.5,0.5,0.5},
		{-0.5,-0.5,0.5},
		{0.5,-0.5,0.5},
		{0.5,0.5,0.5},
		{0.5,0.5,0.5},
		{0.5,-0.5,0.5},
		{0.5,-0.5,-0.5},
		{0.5,0.5,-0.5},
		{-0.5,0.5,-0.5},
		{-0.5,-0.5,-0.5},
		{-0.5,-0.5,0.5},
		{-0.5,0.5,0.5},
		{0.5,0.5,0.5},
		{0.5,0.5,-0.5},
		{-0.5,0.5,-0.5},
		{-0.5,0.5,0.5},
		{-0.5,-0.5,0.5},
		{-0.5,-0.5,-0.5},
		{0.5,-0.5,-0.5},
		{0.5,-0.5,0.5}
	};
	v2_t t[24] = {
		{0,0},
		{0,1},
		{1,1},
		{1,0},
		{0,0},
		{0,1},
		{1,1},
		{1,0},
		{0,0},
		{0,1},
		{1,1},
		{1,0},
		{0,0},
		{0,1},
		{1,1},
		{1,0},
		{0,0},
		{0,1},
		{1,1},
		{1,0},
		{0,0},
		{0,1},
		{1,1},
		{1,0}
	};
	int indices[36] = {
		0,1,3,
		3,1,2,
		4,5,7,
		7,5,6,
		8,9,11,
		11,9,10,
		12,13,15,
		15,13,14,
		16,17,19,
		19,17,18,
		20,21,23,
		23,21,22
	};
	int i;
	mesh_t *m;
	object_t *o = render3d_object_create();
	o->pos.x = pos->x;
	o->pos.y = pos->y;
	o->pos.z = pos->z;
	m = mesh_create_material(mat);
	for (i=0; i<24; i++) {
		array_push_v2t(m->t,&t[i]);
		v[i].x *= scale;
		v[i].y *= scale;
		v[i].z *= scale;
		array_push_v3t(m->v,&v[i]);
	}
	for (i=0; i<36; i++) {
		array_push_int(m->i,indices[i]);
	}
	array_push_ptr(o->meshes,m);
	object_calc_normals(o);
	o->ignore = 0;
	return o;
}

int render3d_point_in_frustum(v3_t *p)
{
	int i;
	for (i=0; i<6; i++) {
		if (((render3d_data.frustum[i][0]*p->x)+(render3d_data.frustum[i][1]*p->y)+(render3d_data.frustum[i][2]*p->z)+render3d_data.frustum[i][3]) <= 0)
			return 0;
	}
	return 1;
}

int render3d_bounds_in_frustum(v3_t *p, aabox_t *b)
{
	v3_t v[8];
	int i;
	int k;

	if (render3d_point_in_frustum(p))
		return 1;

	v[0].x = b->min.x;
	v[0].y = b->min.y;
	v[0].z = b->min.z;

	v[1].x = b->max.x;
	v[1].y = b->min.y;
	v[1].z = b->min.z;

	v[2].x = b->min.x;
	v[2].y = b->max.y;
	v[2].z = b->min.z;

	v[3].x = b->max.x;
	v[3].y = b->max.y;
	v[3].z = b->min.z;

	v[4].x = b->min.x;
	v[4].y = b->min.y;
	v[4].z = b->max.z;

	v[5].x = b->max.x;
	v[5].y = b->min.y;
	v[5].z = b->max.z;

	v[6].x = b->min.x;
	v[6].y = b->max.y;
	v[6].z = b->max.z;

	v[7].x = b->max.x;
	v[7].y = b->max.y;
	v[7].z = b->max.z;

	for (i=0; i<6; i++) {
		for (k=0; k<8; k++) {
			if (((
				render3d_data.frustum[i][0]*v[k].x)
				+(render3d_data.frustum[i][1]*v[k].y)
				+(render3d_data.frustum[i][2]*v[k].z)
				+render3d_data.frustum[i][3]) > 0
			)
				break;
		}
		if (k == 8)
			return 0;
	}

	return 1;
}

/* render 3d graphics to the frame */
void render3d(camera_t *cam, v4_t *plane)
{
	static v4_t p = {1000000.0,0.0,-1.0,0.0};
	int i;
	object_t *o;
	mesh_t *m;
	matrix_t mat;
	matrix_t *projection;
	shader_t *active_shader;
	float t;
	GLuint active_vao = 0;
	if (!render3d_data.objects || !render3d_data.objects->length)
		return;

	if (!render3d_data.shader) {
		render3d_data.shader = shader_create("model");
		shader_attribute(render3d_data.shader,0,"position");
		shader_attribute(render3d_data.shader,1,"normals");
		shader_attribute(render3d_data.shader,2,"texcoords");
		/* possibly a colours attribute as well */
	}

	shader_enable(render3d_data.shader);
	active_shader = render3d_data.shader;

	camera_view_matrix(&render3d_data.view,cam);
	projection = render_get_projection_matrix();

	shader_uniform_matrix(active_shader,"projectionMatrix",projection);
	shader_uniform_matrix(active_shader,"viewMatrix",&render3d_data.view);
	if (plane) {
		glEnable(GL_CLIP_DISTANCE0);
	}else{
		glDisable(GL_CLIP_DISTANCE0);
		plane = &p;
	}
	shader_uniform_v4(active_shader,"plane",plane);

	/* calculate the frustum */
	mat = *projection;
	matrix_multiply(&mat,&render3d_data.view);

	/* Extract the numbers for the RIGHT plane */
	render3d_data.frustum[0][0] = mat.data[ 3] - mat.data[ 0];
	render3d_data.frustum[0][1] = mat.data[ 7] - mat.data[ 4];
	render3d_data.frustum[0][2] = mat.data[11] - mat.data[ 8];
	render3d_data.frustum[0][3] = mat.data[15] - mat.data[12];

	/* Normalize the result */
	t = sqrt( render3d_data.frustum[0][0] * render3d_data.frustum[0][0] + render3d_data.frustum[0][1] * render3d_data.frustum[0][1] + render3d_data.frustum[0][2] * render3d_data.frustum[0][2] );
	render3d_data.frustum[0][0] /= t;
	render3d_data.frustum[0][1] /= t;
	render3d_data.frustum[0][2] /= t;
	render3d_data.frustum[0][3] /= t;

	/* Extract the numbers for the LEFT plane */
	render3d_data.frustum[1][0] = mat.data[ 3] + mat.data[ 0];
	render3d_data.frustum[1][1] = mat.data[ 7] + mat.data[ 4];
	render3d_data.frustum[1][2] = mat.data[11] + mat.data[ 8];
	render3d_data.frustum[1][3] = mat.data[15] + mat.data[12];

	/* Normalize the result */
	t = sqrt( render3d_data.frustum[1][0] * render3d_data.frustum[1][0] + render3d_data.frustum[1][1] * render3d_data.frustum[1][1] + render3d_data.frustum[1][2] * render3d_data.frustum[1][2] );
	render3d_data.frustum[1][0] /= t;
	render3d_data.frustum[1][1] /= t;
	render3d_data.frustum[1][2] /= t;
	render3d_data.frustum[1][3] /= t;

	/* Extract the BOTTOM plane */
	render3d_data.frustum[2][0] = mat.data[ 3] + mat.data[ 1];
	render3d_data.frustum[2][1] = mat.data[ 7] + mat.data[ 5];
	render3d_data.frustum[2][2] = mat.data[11] + mat.data[ 9];
	render3d_data.frustum[2][3] = mat.data[15] + mat.data[13];

	/* Normalize the result */
	t = sqrt( render3d_data.frustum[2][0] * render3d_data.frustum[2][0] + render3d_data.frustum[2][1] * render3d_data.frustum[2][1] + render3d_data.frustum[2][2] * render3d_data.frustum[2][2] );
	render3d_data.frustum[2][0] /= t;
	render3d_data.frustum[2][1] /= t;
	render3d_data.frustum[2][2] /= t;
	render3d_data.frustum[2][3] /= t;

	/* Extract the TOP plane */
	render3d_data.frustum[3][0] = mat.data[ 3] - mat.data[ 1];
	render3d_data.frustum[3][1] = mat.data[ 7] - mat.data[ 5];
	render3d_data.frustum[3][2] = mat.data[11] - mat.data[ 9];
	render3d_data.frustum[3][3] = mat.data[15] - mat.data[13];

	/* Normalize the result */
	t = sqrt( render3d_data.frustum[3][0] * render3d_data.frustum[3][0] + render3d_data.frustum[3][1] * render3d_data.frustum[3][1] + render3d_data.frustum[3][2] * render3d_data.frustum[3][2] );
	render3d_data.frustum[3][0] /= t;
	render3d_data.frustum[3][1] /= t;
	render3d_data.frustum[3][2] /= t;
	render3d_data.frustum[3][3] /= t;

	/* Extract the FAR plane */
	render3d_data.frustum[4][0] = mat.data[ 3] - mat.data[ 2];
	render3d_data.frustum[4][1] = mat.data[ 7] - mat.data[ 6];
	render3d_data.frustum[4][2] = mat.data[11] - mat.data[10];
	render3d_data.frustum[4][3] = mat.data[15] - mat.data[14];

	/* Normalize the result */
	t = sqrt( render3d_data.frustum[4][0] * render3d_data.frustum[4][0] + render3d_data.frustum[4][1] * render3d_data.frustum[4][1] + render3d_data.frustum[4][2] * render3d_data.frustum[4][2] );
	render3d_data.frustum[4][0] /= t;
	render3d_data.frustum[4][1] /= t;
	render3d_data.frustum[4][2] /= t;
	render3d_data.frustum[4][3] /= t;

	/* Extract the NEAR plane */
	render3d_data.frustum[5][0] = mat.data[ 3] + mat.data[ 2];
	render3d_data.frustum[5][1] = mat.data[ 7] + mat.data[ 6];
	render3d_data.frustum[5][2] = mat.data[11] + mat.data[10];
	render3d_data.frustum[5][3] = mat.data[15] + mat.data[14];

	/* Normalize the result */
	t = sqrt( render3d_data.frustum[5][0] * render3d_data.frustum[5][0] + render3d_data.frustum[5][1] * render3d_data.frustum[5][1] + render3d_data.frustum[5][2] * render3d_data.frustum[5][2] );
	render3d_data.frustum[5][0] /= t;
	render3d_data.frustum[5][1] /= t;
	render3d_data.frustum[5][2] /= t;
	render3d_data.frustum[5][3] /= t;

	render3d_data.sorted = NULL;

	for (i=0; i<render3d_data.objects->length; i++) {
		o = ((object_t**)(render3d_data.objects->data))[i];
		/* check if we're meant to touch it */
		if (!o)
			continue;
		if (o->drop)
			o->ignore = 2;
		if (o->ignore) {
			o->drop = 0;
			if (o->ignore == 2)
				o->ignore++;
			continue;
		}
		/* call step on each object, this may modify the data (such as with animated models) */
		if (o->m && o->m->step) {
			o->m->step(o);
			object_calc_bounds(o);
		}else if (o->bounds.min.x == o->bounds.max.x && o->bounds.min.y == o->bounds.max.y && o->bounds.min.z == o->bounds.max.z) {
			object_calc_bounds(o);
		}
		/* ignore anything that can't be seen */
		if (!render3d_bounds_in_frustum(&o->pos,&o->bounds))
			continue;

		/* sort objects by distance */
		render3d_data.sorted = list_insert_cmp(&render3d_data.sorted,o,render3d_sort);
	}

	o = render3d_data.sorted;
	/* now render objects, furthest first */
	while (o) {
		matrix_init(&mat);
		matrix_scale_v(&mat,&o->scale);
		matrix_rotate_deg_z(&mat,o->rot.z);
		matrix_rotate_deg_y(&mat,o->rot.y);
		matrix_rotate_deg_x(&mat,o->rot.x);
		matrix_translate_v(&mat,&o->pos);
		shader_uniform_matrix(active_shader,"transformationMatrix",&mat);
		light_bind_near(render3d_data.shader,&o->pos);
		for (i=0; i<o->meshes->length; i++) {
			m = ((mesh_t**)o->meshes->data)[i];
			/* create buffers if necessary and fill with data */
			if (!m->vao.state)
				render3d_genvao(m);

			if (!m->vao.list)
				continue;

			if (m->vao.list != active_vao) {
				glBindVertexArray(m->vao.list);
				glEnableVertexAttribArray(0);

				/* use vertex, normal, and texcoord or colour arrays */
				if (m->vao.normals) {
					glEnableVertexAttribArray(1);
				}else{
					glDisableVertexAttribArray(1);
				}
				if (m->vao.texcoords) {
					glEnableVertexAttribArray(2);
				}else{
					glDisableVertexAttribArray(2);
				}

				active_vao = m->vao.list;
			}

			mat_use(m->mat,active_shader);

			if (m->option) {
				switch(m->mode) {
				case GL_POINTS:
					glPointSize(m->option);
					break;
				case GL_LINES:
					glLineWidth(m->option);
					break;
				default:;
				}
				glDrawElements(m->mode,m->i->length,GL_UNSIGNED_INT,NULL);
				switch(m->mode) {
				case GL_POINTS:
					glPointSize(1);
					break;
				case GL_LINES:
					glLineWidth(1);
					break;
				default:;
				}
			}else{
				glDrawElements(m->mode,m->i->length,GL_UNSIGNED_INT,NULL);
			}
		}
		o = o->next;
	}

	glDisableVertexAttribArray(0);
	glDisableVertexAttribArray(1);
	glDisableVertexAttribArray(2);
	glBindVertexArray(0);

	shader_disable(active_shader);
}
